package com.wsl.HealthChallenge.controllers;

import java.util.Calendar;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

import javax.jdo.JDOHelper;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import com.google.appengine.api.labs.taskqueue.Queue;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.gdata.client.http.AuthSubUtil;
import com.google.gdata.util.AuthenticationException;
import com.wsl.HealthChallenge.models.ContestEntry;
import com.wsl.HealthChallenge.models.UserEntry;
import com.wsl.HealthChallenge.models.UserStats;
import com.wsl.HealthChallenge.utils.DataStorageUtils;
import com.wsl.HealthChallenge.utils.MapSortingUtils;
import com.wsl.HealthChallenge.utils.PMF;
import com.wsl.HealthChallenge.utils.Utils;

/**
 * 
 * Handles requests to view or sign up for a challenge:
 * 
 * @author mark
 */
@Controller
public class ChallengeHomeController {
    /**
     * Handles request to URLs of the type www.walk2.me/{challengeId}
     * Parameters: 
     *   - signUp true if the user already clicked on a sign up link
     *  
     * Takes care of several different cases:
     *  - user is logged into the app (yes/no)
     *  - user already linked to Google Health before (yes/no)
     *  - user already signed up for this challenge (yes/no)
     *  - user already confirmed by clicking on a sign up link
     *  
     *  Based on these cases it takes different actions and shows different
     *  sign up messages to the user minimizing the needed interaction with the user.
     *  
     * @param challengeId 
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @RequestMapping("/{challengeId}")
    public ModelAndView handleRequest(@PathVariable String challengeId,
            @RequestParam(value="displayedName", defaultValue="") String displayedName,
            HttpServletRequest request, HttpServletResponse response) throws Exception {
        // Check if the challenge exists.
        ContestEntry challenge = DataStorageUtils.getContest(challengeId);
        if (challenge == null) {
            response.getWriter().println("Challenge does not exist.");
            return null;
        }

        UserService userService = UserServiceFactory.getUserService();
        if (!userService.isUserLoggedIn()) {
            // The user is not logged in, so we don't know if he already signed up
            // Show page saying "Sign up for contest or log-in"
            return renderSignUpOrLoginPage(request, challenge, userService);
        }

        User user = userService.getCurrentUser();
        boolean userClickedSignUp = request.getParameter("signUp") != null;

        UserEntry userEntry = DataStorageUtils.getUserEntry(user.getUserId());
        if (userEntry == null || userEntry.getGoogleHealthSessionToken() == null) {
            // The user never linked to his GHealth account or revoke the token.
            // TODO(mark): Show page saying "Link to your Google health account"
            // that links to GHealth login with a next link to "/{challengeId}?linkToGHealth=true".
            String linkToGHealthUrl = "/" + challengeId + "/linkToGHealth";
            if (userClickedSignUp) {
                linkToGHealthUrl += "?signUp=true";
            }
            Utils.forwardToUrl(linkToGHealthUrl, request, response);
            return null;
        }

        UserStats myStats = challenge.getUserStats().get(user.getUserId());
        boolean alreadySignedUp = 
            myStats != null && myStats.getGoogleHealthSessionToken() != null;
        //TODO(mark): check GH token inside userentry

        if(alreadySignedUp) {
            if (!displayedName.isEmpty()) {
                Map<String, UserStats> userStats = challenge.getUserStats();
                UserStats stats = userStats.get(user.getUserId());
                stats.setName(displayedName);
                JDOHelper.makeDirty(challenge, "userStats");
                DataStorageUtils.insertOrUpdateContest(challenge);
            }

            // The user is already signed up for this contest
            return showChallengePage(request, challenge, user);
        }

//        // The user is not already signed up for this contest.
//        boolean userClickedSignUp = request.getParameter("signUp") != null;
//
//        UserEntry userEntry = DataStorageUtils.getUserEntry(user.getUserId());
//        if (userEntry == null || userEntry.getGoogleHealthSessionToken() == null) {
//            // The user never linked to his GHealth account.
//            // TODO(mark): Show page saying "Link to your Google health account"
//            // that links to GHealth login with a next link to "/{challengeId}?linkToGHealth=true".
//            String linkToGHealthUrl = "/" + challengeId + "/linkToGHealth";
//            if (userClickedSignUp) {
//                linkToGHealthUrl += "?signUp=true";
//            }
//            Utils.forwardToUrl(linkToGHealthUrl, request, response);
//            return null;
//        }

        if (!userClickedSignUp) {
            // The user has not yet clicked on sign up.
            return renderSignUpPage(request, challenge);
        }

        // The user is logged into walk2me and Google Health and clicked at least once
        // on a sign up link. Add the user to the challenge and show the challenge page.
        if (myStats != null) {
            // If the user is already in this challenge,but revoked the token and
            // is now coming back (myStats.getGoogleHealthSessionToken()==null)
            // set the new token.
            myStats.setGoogleHealthSessionToken(userEntry.getGoogleHealthSessionToken());
        } else {
            myStats = new UserStats(request.getUserPrincipal().getName(), 0.f,
                    userEntry.getGoogleHealthSessionToken());
        }
        challenge.getUserStats().put(user.getUserId(), myStats);
        JDOHelper.makeDirty(challenge, "userStats"); 
        DataStorageUtils.insertOrUpdateContest(challenge);

        userEntry.getParticipatingContests().add(challenge.getId());
        JDOHelper.makeDirty(userEntry, "participatingContests"); 
        DataStorageUtils.insertOrUpdateUser(userEntry);

        // Trigger a task to compute the users score.
        Queue queue = UpdateScoreController.getUpdateScoreQueue();
        UpdateScoreController.addUpdateParticipantTask(challengeId, challenge,
                user.getUserId(), myStats, queue);

        return showChallengePage(request, challenge, user);
    }

    @RequestMapping("/{challengeId}/leave")
    public ModelAndView handleLeaveChallengeRequest(@PathVariable String challengeId,
            HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (!Utils.ensureUserLoggedIn(request, response)) {
            return null;
        }
        UserService userService = UserServiceFactory.getUserService();
        User user = userService.getCurrentUser();
        String userId = user.getUserId();

        PersistenceManager pm = PMF.get().getPersistenceManager();
        try {
            ContestEntry challenge = null;
            pm.currentTransaction().begin();
            try {
                challenge = pm.getObjectById(ContestEntry.class, challengeId);
                Map<String, UserStats> allUserStats = challenge.getUserStats();
                if (allUserStats.containsKey(userId)) {
                    allUserStats.remove(userId);
                    JDOHelper.makeDirty(challenge, "userStats");
                }
            } catch (JDOObjectNotFoundException e) {
            }
            pm.currentTransaction().commit();
        //} catch (Exception e) {
        //    return false;
        } finally {
            if (pm.currentTransaction().isActive()) {
                pm.currentTransaction().rollback();
            } else {
                // Note: since UserEntry and are not in the same entitygroup we
                // cannot do this in one transaction.
                // TODO(mark): Figure out how to do this the correct way.
                UserEntry userEntry = DataStorageUtils.getUserEntry(user.getUserId());
                Set<String> contests = userEntry.getParticipatingContests();
                contests.remove(challengeId);
                JDOHelper.makeDirty(userEntry, "participatingContests");

                String sessionTokenToBeRevoked = userEntry.getGoogleHealthSessionToken();
                if (contests.size() == 0 && sessionTokenToBeRevoked != null) {
                    // Forget the session token.
                    userEntry.setGoogleHealthSessionToken(null);
                    JDOHelper.makeDirty(userEntry, "googleHealthSessionToken");

                    // Revoke the session token since there is no 
                    // contest anymore that the user is in.
                    ServletContext servletContext = request.getSession().getServletContext();
                    try {
                        AuthSubUtil.revokeToken(sessionTokenToBeRevoked,
                                GoogleHealthAuthUtils.loadPrivateKey(servletContext));
                    } catch(AuthenticationException e) {
                        // We get an AuthenticationException if the token was already been revoked
                        // (e.g. from inside GH).
                        // We ignore this exception, since revoking the token is what we wanted
                        // to do anyways.
                        UpdateScoreController.log.log(Level.SEVERE, 
                                "leave challenge: AuthenticationException: #" +
                                e.getHttpErrorCodeOverride());
                    }
                }
                DataStorageUtils.insertOrUpdateUser(userEntry);
            }
        }

        response.sendRedirect("/"+challengeId);
        return null;
    }

    /**
     * Renders the page telling the user to sign up.
     * We show this to users that are logged in already linked to their Google Health account.
     * @param request
     * @param challenge
     * @return
     */
    private ModelAndView renderSignUpPage(HttpServletRequest request,
            ContestEntry challenge) {
        ModelMap model = new ModelMap();
        model.addAttribute("challengeName", challenge.getName());
        model.addAttribute("userName", request.getUserPrincipal().getName());
        String fullUrl = request.getRequestURL().toString();
        model.addAttribute("signUpUrl", fullUrl + "?signUp=true");
        return new ModelAndView("signup_view", model);
    }

    /**
     * Renders the page telling the user to sign up or login.
     * We show this to users that are not logged in so we don't know
     * if they already signed up for this challenge or not.
     * @param request
     * @param challenge
     * @param userService
     * @return
     */
    private ModelAndView renderSignUpOrLoginPage(HttpServletRequest request,
            ContestEntry challenge, UserService userService) {
        // The user is not logged in, so we don't know if he already signed up
        // Show page saying "Sign up for contest or log-in to see your results"
        // with a link to login to walk2.me.
        String fullUrl = request.getRequestURL().toString();
        ModelMap model = new ModelMap();
        model.addAttribute("challengeName", challenge.getName());
        model.addAttribute("signUpUrl",
                userService.createLoginURL(fullUrl + "?signUp=true"));
        return new ModelAndView("signup_or_login_view", model);
    }

    /**
     * Renders the challenge page.
     * @param request
     * @param challenge
     * @param user
     * @return
     */
    private ModelAndView showChallengePage(HttpServletRequest request,
            ContestEntry challenge, User user) {
        ModelMap model = new ModelMap();
        model.addAttribute("challengeName", challenge.getName());
        model.addAttribute("challengeType", challenge.getType());
        model.addAttribute("challengeDescription", challenge.getDescription());
        model.addAttribute("leaveChallengeUrl", "/" + challenge.getId() + "/leave");
        
        model.addAttribute("isUserSignedUp", true);
        model.addAttribute("isUserLinked", true);

        Map<String, UserStats> userStats = challenge.getUserStats();

        Map<String, UserStats> sortedUserStats = 
            MapSortingUtils.sortMapByValue(userStats, MapSortingUtils.SortingOrder.DESCENDING);

        UserStats stats = userStats.get(user.getUserId());
        float userScore = stats.getScore();

        model.addAttribute("userId", user.getUserId());
        model.addAttribute("userScore", String.valueOf(userScore));
        model.addAttribute("userName", request.getUserPrincipal().getName());
        model.addAttribute("userNickname", user.getNickname());
        model.addAttribute("displayedUserName", stats.getName());
        model.addAttribute("userEmail", user.getEmail());

        model.addAttribute("numParticipants", String.valueOf(userStats.size()));
        model.addAttribute("daysSinceStart", 
                new Integer(computeDaysSinceStart(challenge.getStartDate())));
        
        model.addAttribute("shouldShowNoCalorieNotification", (userScore == 0.0));

        // TODO(mark): Implement functions to compute the top ten and closeby users.
        model.addAttribute("topTenUsers", getTop10Users(sortedUserStats).entrySet());
        model.addAttribute("closeByUsers",
                get10UsersRankedNearMe(sortedUserStats, user).entrySet());
        return new ModelAndView("view_challenge_view", model);
    }

    /**
     * @return the number of full days that have passed from now since the start of the challenge. 
     */
    private int computeDaysSinceStart(Date startDate) {
        long numMillisecondsSinceStart = (Calendar.getInstance().getTimeInMillis() 
                - startDate.getTime()); 
        return (int)(numMillisecondsSinceStart / 86400000);
    }

    /**
     * A simple method to return top 10 users in this challenge. There is likely much
     * more efficient ways to do this.
     * 
     * @param sortedUserStats
     * @return
     */
    private Map<String, UserStats> getTop10Users(Map<String, UserStats> sortedUserStats) {
        LinkedHashMap<String, UserStats> top10 = new LinkedHashMap<String, UserStats>();
        for(Map.Entry<String, UserStats> entry : sortedUserStats.entrySet()) {
            top10.put(entry.getKey(), entry.getValue());
            if (top10.size() == 10) {
                break;
            }
        }
        return top10;
    }

    /**
     * A simple method to return 10 users near-by in ranking to the current user. There is likely 
     * much more efficient and concise ways to do this, but this is the one that comes to mind.
     * 
     * @param sortedUserStats
     * @param me
     * @return
     */
    private Map<String, UserStats> get10UsersRankedNearMe(Map<String, UserStats> sortedUserStats, User me) {
        int myRanking = 0;
        int minRankingToReturn = 0;

        for(Map.Entry<String, UserStats> entry : sortedUserStats.entrySet()) {
            if (entry.getKey().equals(me.getUserId())) {
                // Found the current user in all user stats, now need to add the 5 users before this and user and 4 after,
                // but only if they exist.
                int peopleAfterMe = Math.min(4, sortedUserStats.size() - myRanking - 1);
                minRankingToReturn = Math.max(0, myRanking - (10 - peopleAfterMe - 1));
                break;
            }
            myRanking++;
        }

        // Now return 10 entries starting at minRankingToReturn
        int curRanking = 0;
        LinkedHashMap<String, UserStats> nearby10 = new LinkedHashMap<String, UserStats>();
        for(Map.Entry<String, UserStats> entry : sortedUserStats.entrySet()) {
            if (curRanking >= minRankingToReturn) {
                nearby10.put(entry.getKey(), entry.getValue());
                if (nearby10.size() == 10) {
                    break;
                }
            }
            curRanking++;
        }

        return nearby10;
    }
}